뒤죽박죽 코드블록 컴포넌트 뜯어 고치기
뒤죽박죽의 코드블록 컴포넌트를 고쳐보자
2024-04-16
문제 정의
먼저 블로그의 코드블록은 react-syntax-highlight라는 패키지를 쓰고 있었습니다. 이 패키지는 마크다운 형식의 코드블록을 이쁘게 하이라이팅 해주는 라이브러리입니다.
아래처럼 사용하면 다음과 같은 형태로 실제 화면에 나오게 됩니다. 저는 이 코드 블록 컴포넌트를 블로그 상세 글 페이지에서 사용하고 있었습니다. 정확히는 posts폴더의 [id]/page.tsx에서 사용하고 있었습니다!
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { dark } from 'react-syntax-highlighter/dist/esm/styles/prism';
const Component = () => {
const codeString = '(num) => num + 1';
return (
<SyntaxHighlighter language="javascript" style={dark}>
{codeString}
</SyntaxHighlighter>
);
};
먼저 처음 나타났던 문제는 너무 큰 번들의 크기였습니다. 공식문서에서처럼 사용하게 된다면 너무 번들의 크기가 커졌습니다.
그럼 의문이 하나 생깁니다. 왜 이렇게 번들이 클 것일까요? 먼저 해당 라이브러리의 default-highlight.js파일과 prism.js파일에서 어렴풋이 알 수 있었습니다.
import highlight from './highlight';
import defaultStyle from './styles/hljs/default-style';
import lowlight from 'lowlight';
import supportedLanguages from './languages/hljs/supported-languages';
const highlighter = highlight(lowlight, defaultStyle);
highlighter.supportedLanguages = supportedLanguages;
export default highlighter;
보면 highlighter 객체에 supportedLanguages 배열을 속성으로 추가하여, 지원하는 언어 목록을 지정합니다. 이 지원하는 언어 목록 배열은 굉장히 크기가 큰 것을 볼 수 있었습니다.
이런 문제를 보고 , 가장 처음에 든 생각은 Dynamic import를 사용하는 것이었습니다.
코드블록 컴포넌트는 먼저 블로그 글의 상세페이지에서만 필요한 컴포넌트였습니다. 따라서 초기 로드 때 글의 목록페이지에서 이를 불러올 필요가 없었고, 동적 import를 사용했습니다. 다음과 같이 next의 dynamic을 사용해서 기존의 코드블록 컴포넌트를 수정하였습니다.
//CodeBlock.tsx
'use client';
import dynamic from 'next/dynamic';
const SyntaxHighlighter = dynamic(() => import('react-syntax-highlighter'));
import { atomDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
export default function CodeBlock({
children,
}: {
children: string | string[];
}) {
return (
<SyntaxHighlighter
language="javascript"
wrapLines={true}
style={atomDark}
lineProps={{
style: {
wordBreak: 'break-all',
whiteSpace: 'pre-wrap',
fontSize: '16px',
},
}}
>
{children}
</SyntaxHighlighter>
);
}
물론 Dynamic Import를 통해 번들 사이즈를 줄였지만, 더 확실하게 고치고 싶었습니다. 동적 import를 사용해도 다음의 문제는 아직 남아있는 상태였습니다.
- 👀 language를 자바스크립트로 고정해두었습니다. 마크다운 입력 시 많은 언어의 입력에 대한 케이스를 처리해야만 했습니다.
- 👀 Dynamic Import로 번들 사이즈를 줄였지만, 그래도 굉장히 번들이 컸습니다.
저는 이 두 가지 문제를 해결하고자 하였습니다.
문제 해결 과정
먼저 Dynamic Import로 줄인 번들 사이즈를 더 획기적으로 줄이고 싶었습니다. 이렇게 하려면, 여러 방법이 있을 것 같았습니다.
- ⚪ 번들 크기를 줄이기 위해 서버 컴포넌트와 호환되는 마크다운 하이라이트 패키지로 갈아끼우는 방법
- ⚪ 이미 설치되어 있는 react-syntax-highlight에서 어떻게든 번들 크기를 줄이는 방법
첫번째 방법은 , 아예 새로운 패키지를 찾고, 도입하는 과정이기 때문에 비용이 많이 들 것 같았습니다.
또 아무리 찾아봐도 서버 컴포넌트와 호환되는 마크다운 패키지가 그렇게 많지는 않더라구요..!.. 결정적으로 오기가 생겼습니다. 이미 있는 패키지를 번들 크기 때문에 갈아끼우고 싶진 않았습니다.
오히려 새 라이브러리를 찾고 교체하는 것이 부자연스럽다고도 느꼈어요! 따라서 이미 설치되어 있는 react-syntax-highlight에서 어떻게든 번들 크기를 줄이고자 하였습니다.
(부끄럽지만, 해당 라이브러리로 가서 어떻게 번들 크기를 줄이는지 물어보기도 하였습니다. 관련 주소)
앞서 코드를 보면 지원하는 모든 언어들을 개발자가 선택적으로 가져올 수 없을까?하는 의문이 생깁니다. 예를 들어, 실제 블로그 글을 쓸 때 주로 사용되는 언어는 JSON,ts,js,tsx등의 확장자입니다. 이렇게 개발자가 선택한 언어만 등록해둔다면, 번들 크기가 엄청 획기적으로 줄 수 있을 것 같았습니다.
다행히 react-syntax-hightlight에서는 위의 기능을 제공해줍니다. 해당 라이브러리의 light.js파일을 가져와보았습니다.
import highlight from './highlight';
import lowlight from 'lowlight/lib/core';
const SyntaxHighlighter = highlight(lowlight, {});
SyntaxHighlighter.registerLanguage = lowlight.registerLanguage;
export default SyntaxHighlighter;
보면 앞서 기본적으로 지정해둔 여러 언어 배열은 없습니다. 다만 SyntaxHighlighter.registerLanguage 이 속성에 lowlight.registerLanguage 함수를 직접 할당함으로써, SyntaxHighlighter 객체를 통해 새로운 언어를 등록할 수 있게 합니다.
다음과 같이 사용할 수 있게 됩니다.
import { PrismLight as SyntaxHighlighter } from 'react-syntax-highlighter';
import jsx from 'react-syntax-highlighter/dist/esm/languages/prism/jsx';
import prism from 'react-syntax-highlighter/dist/esm/styles/prism/prism';
SyntaxHighlighter.registerLanguage('jsx', jsx);
그럼 저에게 필요한 언어만 선택적으로 등록해보겠습니다! 저는 총 2개의 작업을 진행하였습니다.
-
🤔 외부에서 언어를 입력받아 코드블록 컴포넌트에 외부에서 주입된 언어를 넣어줍니다.(지금 상황에서는 json,ts,tsx,jsx로만 한정적으로 받습니다.)
-
🤔 블로그 글에 쓰이는 언어가 몇 개 없기 떄문에 해당 언어만 registerLanguage로 등록해, 번들 크기를 줄입니다.
최종적으로 다음의 코드 블록 컴포넌트를 만들 수 있었습니다.
(클라이언트 컴포넌트이기 떄문에 'use client'를 붙입니다. 안그러면 해당 에러가 나옵니다 😭😭)
//최종적으로 개선된 코드 블록 컴포넌트
'use client';
import dynamic from 'next/dynamic';
import bash from 'react-syntax-highlighter/dist/cjs/languages/prism/bash';
import javascript from 'react-syntax-highlighter/dist/cjs/languages/prism/javascript';
import json from 'react-syntax-highlighter/dist/cjs/languages/prism/json';
import jsx from 'react-syntax-highlighter/dist/cjs/languages/prism/jsx';
import tsx from 'react-syntax-highlighter/dist/cjs/languages/prism/tsx';
import typescript from 'react-syntax-highlighter/dist/cjs/languages/prism/typescript';
const PrismLight = dynamic(() =>
import('react-syntax-highlighter/dist/esm/prism-light').then((res) => {
res.default.registerLanguage('ts', typescript);
res.default.registerLanguage('jsx', jsx);
res.default.registerLanguage('js', javascript);
res.default.registerLanguage('json', json);
res.default.registerLanguage('bash', bash);
res.default.registerLanguage('tsx',tsx)
return res;
})
);
import { materialDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
export default function CodeBlock({
children,
lang,
}: {
children: string | string[];
lang?: string;
}) {
return (
<PrismLight
language={lang}
style={materialDark}
customStyle={{
fontFamily: '__Do_Hyeon_7b3cf7',
maxWidth: '100vw',
wordBreak: 'break-all',
fontSize: '1rem',
overflowWrap: 'break-word',
}}
codeTagProps={{
style: {
fontFamily: 'inherit',
wordBreak: 'break-all',
overflowWrap: 'break-word',
},
}}
>
{children}
</PrismLight>
);
}
그럼 해당 번들의크기는 어떻게 바뀌었을까요?
놀랍도록 많이 번들의 크기가 줄어든 것을 볼 수 있었습니다!
정말 뜻깊은 경험이었습니다. 이미 있는 라이브러리를 교체하지 않고 번들의 크기를 줄여보았던 귀중한 경험이었고 뿌듯함도 많이 느낄 수 있었습니다. 😳